// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.scene;

import bats.SpatialProperties;
import bats.map.MapNode;
import bats.missile.Shafts;
import bats.unit.Bobs;
import bats.unit.Otos;
import bats.unit.Sinbads;
import bats.unit.TargetControl;
import bats.unit.UnitClass;
import bats.unit.ViewpointControl;
import com.jme3.audio.AudioNode;
import com.jme3.collision.Collidable;
import com.jme3.collision.CollisionResults;
import com.jme3.collision.UnsupportedCollisionException;
import com.jme3.effect.ParticleEmitter;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.scene.SceneGraphVisitor;
import com.jme3.scene.Spatial;
import java.util.logging.Level;
import java.util.logging.Logger;
import jme3utilities.MySpatial;
import jme3utilities.MyString;
import jme3utilities.math.VectorXZ;

/**
 * A node control to manage the lights, lit objects, and physics of a scene.
 * <p>
 * The control's spatial is a Node, to which flames, launched missiles, lights,
 * non-pickable props, and impaired units may be attached directly. Pickable
 * spatials should be attached to PickablesNode instead.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class WorldNode
        extends NodeControl {
    // *************************************************************************
    // constants

    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(WorldNode.class.getName());
    /**
     * name for the node in the scene graph
     */
    final private static String nodeName = "world";
    // *************************************************************************
    // fields
    /**
     * "bob" units in this world: set by constructor
     */
    final private Bobs bobs;
    /**
     * true to render physics debug visualizations, false to hide the
     * visualizations; valid only while this world is disabled
     */
    private boolean willDebugPhysics = false;
    /**
     * flames in this world: set by constructor
     */
    final private Flames flames;
    /**
     * map for this world: set by constructor
     */
    final private MapNode map;
    /**
     * "oto" units in this world: set by constructor
     */
    final private Otos otos;
    /**
     * physics for this world: set by constructor
     */
    final private Physics physics;
    /**
     * pickables in this world: set by constructor
     */
    final private PickablesNode pickables;
    /**
     * props in this world: set by constructor
     */
    final private Props props;
    /**
     * shafted missiles in this world: set by constructor
     */
    final private Shafts shafts;
    /**
     * "sinbad" units in this world: set by constructor
     */
    final private Sinbads sinbads;
    // *************************************************************************
    // constructors

    /**
     * Instantiate the enabled control for a specified scene. After
     * instantiation, the first method invoked should be initialize().
     *
     * @param scene which scene will contain this control (not null)
     * @param radius distance from origin to nearest map edge (in meters, &gt;0)
     */
    public WorldNode(SceneNode scene, float radius) {
        super(scene);

        assert radius > 0f : radius;

        bobs = new Bobs(scene);
        flames = new Flames(scene);
        map = new MapNode(scene, radius);
        otos = new Otos(scene);
        physics = new Physics(scene);
        pickables = new PickablesNode(scene);
        props = new Props(scene);
        shafts = new Shafts(scene);
        sinbads = new Sinbads(scene);

        assert isEnabled();
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Run a geometric collision test on this world.
     *
     * @param other the collidable to test against (not null)
     * @return a new list of collisions
     */
    public CollisionResults collideWith(Collidable other)
            throws UnsupportedCollisionException {
        assert other != null;

        CollisionResults results = new CollisionResults();
        spatial.collideWith(other, results);
        return results;
    }

    /**
     * Find the world which contains the specified spatial.
     *
     * @param start which spatial (not null)
     * @return the pre-existing instance
     */
    public static WorldNode find(Spatial start) {
        assert start != null;

        Spatial current = start;
        WorldNode result = current.getControl(WorldNode.class);
        while (result == null) {
            current = current.getParent();
            result = current.getControl(WorldNode.class);
        }
        return result;
    }

    /**
     * Access the bobs in this world.
     *
     * @return the pre-existing instance (not null)
     */
    public Bobs getBobs() {
        assert bobs != null;
        return bobs;
    }

    /**
     * Access the flames in this world.
     *
     * @return the pre-existing instance (not null)
     */
    public Flames getFlames() {
        assert flames != null;
        return flames;
    }

    /**
     * Access the world map in this world.
     *
     * @return the pre-existing instance (not null)
     */
    public MapNode getMap() {
        assert map != null;
        return map;
    }

    /**
     * Access the otos in this world.
     *
     * @return the pre-existing instance (not null)
     */
    public Otos getOtos() {
        assert otos != null;
        return otos;
    }

    /**
     * Access the physics of this world.
     *
     * @return the pre-existing instance (not null)
     */
    public Physics getPhysics() {
        assert physics != null;
        return physics;
    }

    /**
     * Access the pickables in this world.
     *
     * @return the pre-existing instance (not null)
     */
    public PickablesNode getPickables() {
        assert pickables != null;
        return pickables;
    }

    /**
     * Access the props in this world.
     *
     * @return the pre-existing instance (not null)
     */
    public Props getProps() {
        assert props != null;
        return props;
    }

    /**
     * Access the shafts in this world.
     *
     * @return the pre-existing instance (not null)
     */
    public Shafts getShafts() {
        assert shafts != null;
        return shafts;
    }

    /**
     * Access the sinbads in this world.
     *
     * @return the pre-existing instance (not null)
     */
    public Sinbads getSinbads() {
        assert sinbads != null;
        return sinbads;
    }

    /**
     * Determine which class a unit belongs to.
     *
     * @param spatial which unit to classify
     * @return a pre-existing instance (or null is the spatial is not a unit)
     */
    public UnitClass getUnitClass(Spatial spatial) {
        if (bobs.isAnInstance(spatial)) {
            return bobs;
        } else if (otos.isAnInstance(spatial)) {
            return otos;
        } else if (sinbads.isAnInstance(spatial)) {
            return sinbads;
        } else {
            return null;
        }
    }

    /**
     * Alter the world when switching from simulation mode to world-building
     * mode.
     */
    public void goBuild() {
        flames.extinguishAll();
        pickables.deleteAll();
        for (Spatial child : getChildren()) {
            if (SpatialProperties.isProp(child)) {
                pickables.add(child);
            } else if (pickables.getNode() != child
                    && map.getNode() != child) {
                scene.deleteSpatial(child);
            }
        }
        map.reload();
        map.makeBlocksPickable();
    }

    /**
     * Alter the world when switching from world-building mode to simulation
     * mode.
     */
    public void goSimulation() {
        pickables.makeUnpickable();
    }

    /**
     * Initialize this control.
     */
    public void initialize() {
        assert spatial == null : spatial;

        Node node = new Node(nodeName);
        node.addControl(this);
        /*
         * Attach the node to the scene graph as a reflectible.
         */
        scene.getWater().attachReflectible(spatial);

        map.initialize();
        pickables.initialize();

        assert spatial != null;
        assert !MySpatial.isOrphan(spatial);
    }

    /**
     * Process an action which has just begun.
     *
     * @param words action string split into an array of words (not null)
     * @return true if the action was handled, false if it wasn't handled
     */
    public boolean processAction(String[] words) {
        String verb = words[0];
        switch (verb) {
            case "add":
                if (words.length == 2) {
                    String what = words[1];
                    switch (what) {
                        case "fire":
                            addFire();
                            return true;
                    }
                } else if (words.length == 3) {
                    String what = words[1];
                    String which = words[2];
                    switch (what) {
                        case "unit":
                            addUnit(which);
                            return true;
                    }
                }
                break;

            case "delete":
                if (words.length == 2) {
                    String what = words[1];
                    if ("flames".equals(what)) {
                        flames.extinguishAll();
                        return true;
                    }
                }
                break;

            case "toggle":
                if (words.length == 3 && words[2].equals("all")) {
                    switch (words[1]) {
                        case "target":
                            /*
                             * Toggle target visibility.
                             */
                            toggleTargetAll();
                            return true;
                        case "viewpoint":
                            /*
                             * Toggle visibility of unit viewpoints.
                             */
                            toggleViewpointAll();
                            return true;
                    }
                }
        }

        boolean wasHandled = map.processAction(words);
        if (wasHandled) {
            return true;
        }
        wasHandled = physics.processAction(words);
        if (wasHandled) {
            return true;
        }
        wasHandled = pickables.processAction(words);
        if (wasHandled) {
            return true;
        }
        wasHandled = props.processAction(words);
        if (wasHandled) {
            return true;
        }
        return false; // not handled
    }

    /**
     * Enable or disable this world as its scene gets attached or detached.
     *
     * @param newState true to enable, false to disable
     */
    @Override
    public void setEnabled(boolean newState) {
        if (!newState && isEnabled()) {
            willDebugPhysics = physics.isDebugging();
            physics.setDebugging(false);
            /*
             * Mute all audio nodes, freeze all particle emitters.
             */
            SceneGraphVisitor visitor = new SceneGraphVisitor() {
                @Override
                public void visit(Spatial spatial) {
                    if (spatial instanceof AudioNode) {
                        ((AudioNode) spatial).setVolume(0f);
                    }
                    if (spatial instanceof ParticleEmitter) {
                        ((ParticleEmitter) spatial).setEnabled(false);
                    }
                }
            };
            traverse(visitor);

        } else if (newState && !isEnabled()) {
            physics.setDebugging(willDebugPhysics);
            /*
             * Unfreeze all particle emitters.
             */
            SceneGraphVisitor visitor = new SceneGraphVisitor() {
                @Override
                public void visit(Spatial spatial) {
                    if (spatial instanceof ParticleEmitter) {
                        ((ParticleEmitter) spatial).setEnabled(true);
                    }
                }
            };
            traverse(visitor);
        }

        super.setEnabled(newState);
    }

    /**
     * Alter target visibility for all units.
     *
     * @param newState true to make visible, false to hide
     */
    public void setTargetsAll(final boolean newState) {
        SceneGraphVisitor visitor = new SceneGraphVisitor() {
            @Override
            public void visit(Spatial spatial) {
                SpatialProperties.setTargetsVisible(spatial, newState);
            }
        };
        traverse(visitor);
    }

    /**
     * Alter viewpoint visibility for all units.
     *
     * @param newState true to make visible, false to hide
     */
    public void setViewpointAll(final boolean newState) {
        SceneGraphVisitor visitor = new SceneGraphVisitor() {
            @Override
            public void visit(Spatial spatial) {
                SpatialProperties.setViewpointVisible(spatial, newState);
            }
        };
        traverse(visitor);
    }
    // *************************************************************************
    // private methods

    /**
     * Light a fire at the insertion point.
     */
    private void addFire() {
        Vector3f location =
                scene.getIndicators().getCursor3D().getWorldLocation();
        float size = 1f;
        if (scene.getApplication().getSignals().test("alt")) {
            size = 2f;
        }
        float duration = 15f; // in seconds
        getFlames().add(location, size, duration);
    }

    /**
     * Add a north-facing unit at the insertion point and include it in the
     * pick.
     *
     * @param unitType which kind of unit to add (not null)
     */
    private void addUnit(String unitType) {
        assert unitType != null;

        boolean clearPick = !scene.getApplication().getSignals().test("shift");
        if (clearPick) {
            getPickables().clearPick();
        }

        Vector3f location =
                scene.getIndicators().getCursor3D().getWorldLocation();
        VectorXZ direction = new VectorXZ(1f, 0f);
        float big = 2f;
        float mini = 0.5f;
        switch (unitType) {
            case "bigBob":
                bobs.add(big, "bob", location, direction);
                break;
            case "bigOto":
                otos.add(big, "oto", location, direction);
                break;
            case "bob":
                bobs.add("bob", location, direction);
                break;
            case "miniBob":
                bobs.add(mini, "bob", location, direction);
                break;
            case "nan":
                bobs.add("nan", location, direction);
                break;
            case "oto":
                otos.add("oto", location, direction);
                break;
            case "sinbad":
                sinbads.add("sinbad", location, direction);
                break;
            default:
                logger.log(Level.SEVERE, "Unknown unit type {0}",
                        MyString.quote(unitType));
        }
    }

    /**
     * Toggle target visibility for all units.
     */
    private void toggleTargetAll() {
        SceneGraphVisitor visitor = new SceneGraphVisitor() {
            @Override
            public void visit(Spatial spatial) {
                TargetControl targetControl =
                        spatial.getControl(TargetControl.class);
                if (targetControl != null) {
                    targetControl.toggleEnabled();
                }
            }
        };
        traverse(visitor);
    }

    /**
     * Toggle viewpoint visibility for all units.
     */
    private void toggleViewpointAll() {
        SceneGraphVisitor visitor = new SceneGraphVisitor() {
            @Override
            public void visit(Spatial spatial) {
                ViewpointControl viewpointControl =
                        spatial.getControl(ViewpointControl.class);
                if (viewpointControl != null) {
                    viewpointControl.toggleEnabled();
                }
            }
        };
        traverse(visitor);
    }
}